/*
 * Copyright 2002-2007 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.example.socket.codec.protobuf;

import com.baidu.bjf.remoting.protobuf.EnumReadable;
import com.baidu.bjf.remoting.protobuf.FieldType;
import com.baidu.bjf.remoting.protobuf.annotation.Ignore;
import com.baidu.bjf.remoting.protobuf.annotation.Protobuf;
import com.baidu.bjf.remoting.protobuf.utils.FieldInfo;
import com.baidu.bjf.remoting.protobuf.utils.ProtobufProxyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.*;

/**
 *
 * Utility class for generate protobuf IDL content from @{@link Protobuf}
 * @author frank
 * @since 1.0.1
 */
public class ProtobufIDLGenerator {

    /** Logger for this class. */
    private static final Logger LOGGER = LoggerFactory.getLogger(ProtobufIDLGenerator.class.getName());

    /** The Constant V3_HEADER. */
    private static final String V3_HEADER = "syntax=\"proto3\"";

    private static final String PROTO_FILE_SUFFIX = ".proto";

    /** BASE PACKAGE*/
    public static String BASE_PKG;

    /**
     * get IDL content from class.
     *
     * @param cls target class to parse for IDL message.
     * @param cachedTypes if type already in set will not generate IDL. if a new type found will add to set
     * @param cachedEnumTypes if enum already in set will not generate IDL. if a new enum found will add to set
     * @param ignoreJava set true to ignore generate package and class name
     * @return protobuf IDL content in string
     * @see Protobuf
     */
    public static void getIDL(final Class<?> cls, final Set<Class<?>> cachedTypes,
                              final Set<Class<?>> cachedEnumTypes, Map<String, ProtoFile> protoFiles, boolean ignoreJava) {
        Ignore ignore = cls.getAnnotation(Ignore.class);
        IgnoreProto ignoreProto = cls.getAnnotation(IgnoreProto.class);
        if (ignore != null || ignoreProto != null) {
            LOGGER.info("class '{}' marked as @Ignore annotation, create IDL ignored.", cls.getName());
            return;
        }
        Set<Class<?>> types = cachedTypes;
        if (types == null) {
            types = new HashSet<>();
        }
        Set<Class<?>> enumTypes = cachedEnumTypes;
        if (enumTypes == null) {
            enumTypes = new HashSet<>();
        }
        if (protoFiles == null) {
            protoFiles = new HashMap<>();
        }
        if (types.contains(cls)) {
            return;
        }
        types.add(cls);
        ProtoFile protoFile = getProtoFile(cls, protoFiles);
        generateIDL(protoFile, protoFiles, cls, types, enumTypes);
    }

    /**
     *
     * 以包为单位，获取protoFile文件名
     */
    private static String getFileName(Class<?> cls) {
        String name = cls.getName();
        name = name.replaceAll(BASE_PKG, "");
        //把包名分割出来
        int idx = name.lastIndexOf(".");
        name = name.substring(0, idx).replaceAll("\\.", "_");
        //点替换成下划线
        return name + PROTO_FILE_SUFFIX;
    }

    private static ProtoFile getProtoFile(Class<?> cls, Map<String, ProtoFile> protoFiles) {
        String fileName = getFileName(cls);
        ProtoFile protoFile = protoFiles.get(fileName);
        if (protoFile != null) {
            return protoFile;
        }
        protoFile = ProtoFile.valueOf(fileName, cls.getPackage().getName());
        StringBuilder header = protoFile.getHeader();
        header.append(V3_HEADER).append(";\n");
        protoFiles.put(fileName, protoFile);
        return protoFile;
    }

    /**
     * get IDL content from class.
     *
     * @param cls target class to parse for IDL message.
     * @param cachedTypes if type already in set will not generate IDL. if a new type found will add to set
     * @param cachedEnumTypes if enum already in set will not generate IDL. if a new enum found will add to set
     * @return protobuf IDL content in string
     * @see Protobuf
     */
    public static void getIDL(final Class<?> cls, final Set<Class<?>> cachedTypes,
                              final Set<Class<?>> cachedEnumTypes, Map<String, ProtoFile> protoFiles) {
        getIDL(cls, cachedTypes, cachedEnumTypes, protoFiles, false);
    }

    /**
     * @param
     * @param cls
     * @return sub message class list
     */
    private static void generateIDL(ProtoFile protoFile, Map<String, ProtoFile> protoFiles, Class<?> cls, Set<Class<?>> cachedTypes,
                                    Set<Class<?>> cachedEnumTypes) {
        if (cls.getAnnotation(IgnoreProto.class) != null) {
            return;
        }
        StringBuilder code = protoFile.getBody();
        Set<Class<?>> subTypes = new HashSet<>();
        Set<Class<Enum>> enumTypes = new HashSet<>();
        code.append("message ").append(cls.getSimpleName()).append(" {  \n");

        List<FieldInfo> fieldInfos = ProtobufProxyUtils.fetchFieldInfos(cls, false);
        boolean isMap = false;
        for (FieldInfo field : fieldInfos) {
            if (field.getField().getName().equals("Companion")) {
                continue;
            }
            Class c = null;
            if (field.hasDescription()) {
                code.append("// ").append(field.getDescription()).append("\n");
            }
            if (field.getFieldType() == FieldType.OBJECT || field.getFieldType() == FieldType.ENUM) {
                if (field.isList()) {
                    Type type = field.getField().getGenericType();
                    if (type instanceof ParameterizedType) {
                        ParameterizedType ptype = (ParameterizedType) type;

                        Type[] actualTypeArguments = ptype.getActualTypeArguments();
                        if (actualTypeArguments != null && actualTypeArguments.length > 0) {
                            Type targetType = actualTypeArguments[0];
                            if (targetType instanceof Class || targetType instanceof WildcardType) {
                                if(targetType instanceof Class){
                                    c = (Class) targetType;
                                }else {
                                    c = (Class) ((WildcardType)targetType).getUpperBounds()[0];
                                }
                                String fieldTypeName;
                                if (ProtobufProxyUtils.isScalarType(c)) {

                                    FieldType fieldType = ProtobufProxyUtils.TYPE_MAPPING.get(c);
                                    fieldTypeName = fieldType.getType();

                                } else {
                                    if (field.getFieldType() == FieldType.ENUM) {
                                        if (!cachedEnumTypes.contains(c)) {
                                            cachedEnumTypes.add(c);
                                            enumTypes.add(c);
                                        }
                                    } else {

                                        if (!cachedTypes.contains(c)) {
                                            cachedTypes.add(c);
                                            subTypes.add(c);
                                        }
                                    }

                                    fieldTypeName = c.getSimpleName();
                                }

                                code.append("repeated ").append(fieldTypeName).append(" ")
                                        .append(field.getField().getName()).append("=").append(field.getOrder())
                                        .append(";\n");
                            }
                        }
                    }
                } else {
                    c = field.getField().getType();
                    code.append(getFieldRequired(field.isRequired())).append(" ").append(c.getSimpleName()).append(" ")
                            .append(field.getField().getName()).append("=").append(field.getOrder()).append(";\n");
                    if (field.getFieldType() == FieldType.ENUM) {
                        if (!cachedEnumTypes.contains(c)) {
                            cachedEnumTypes.add(c);
                            enumTypes.add(c);
                        }
                    } else {

                        if (!cachedTypes.contains(c)) {
                            cachedTypes.add(c);
                            subTypes.add(c);
                        }
                    }
                }
            } else {
                String type = field.getFieldType().getType().toLowerCase();

                if (field.getFieldType() == FieldType.ENUM) {
                    // if enum type
                    c = field.getField().getType();
                    if (Enum.class.isAssignableFrom(c)) {
                        type = c.getSimpleName();
                        if (!cachedEnumTypes.contains(c)) {
                            cachedEnumTypes.add(c);
                            enumTypes.add(c);
                        }
                    }
                } else if (field.getFieldType() == FieldType.MAP) {
                    isMap = true;
                    Class keyClass = field.getGenericKeyType();
                    Class valueClass = field.getGenericeValueType();
                    type = type + "<" + ProtobufProxyUtils.processProtobufType(keyClass) + ", ";
                    type = type + ProtobufProxyUtils.processProtobufType(valueClass) + ">";

                    // check map key or value is object type
                    if (ProtobufProxyUtils.isObjectType(keyClass)) {
                        if (Enum.class.isAssignableFrom(keyClass)) {
                            enumTypes.add(keyClass);
                        } else {
                            subTypes.add(keyClass);
                        }
                    }

                    if (ProtobufProxyUtils.isObjectType(valueClass)) {
                        if (Enum.class.isAssignableFrom(valueClass)) {
                            enumTypes.add(valueClass);
                        } else {
                            subTypes.add(valueClass);
                        }
                    }

                }

                String required = getFieldRequired(field.isRequired());
                if (isMap) {
                    required = "";
                }

                if (field.isList()) {
                    required = "repeated";
                }

                code.append(required).append(" ").append(type).append(" ").append(field.getField().getName())
                        .append("=").append(field.getOrder()).append(";\n");
            }
            //添加依赖
            if (c != null && !ProtobufProxyUtils.isScalarType(c)) {
                String dependFileName = getFileName(c);
                if (!dependFileName.equals(protoFile.getFileName())) {
                    protoFile.addImport(dependFileName);
                }
            }
        }

        code.append("}\n\n");

        for (Class<Enum> subType : enumTypes) {
            if (subType.getAnnotation(IgnoreProto.class) != null) {
                return;
            }
            ProtoFile subProtoFile = getProtoFile(subType, protoFiles);
            if (subProtoFile == protoFile) {
                generateEnumIDL(protoFile, subType);
            } else {
                generateEnumIDL(subProtoFile, subType);
            }
        }

        if (subTypes.isEmpty()) {
            return;
        }

        for (Class<?> subType : subTypes) {
            if (subType.getAnnotation(IgnoreProto.class) != null) {
                return;
            }
            ProtoFile subProtoFile = getProtoFile(subType, protoFiles);
            if (subProtoFile == protoFile) {
                generateIDL(protoFile, protoFiles, subType, cachedTypes, cachedEnumTypes);
            } else {
                generateIDL(subProtoFile, protoFiles, subType, cachedTypes, cachedEnumTypes);
            }
        }
    }

    private static void generateEnumIDL(ProtoFile protoFile, Class<Enum> cls) {
        if (cls.getAnnotation(IgnoreProto.class) != null) {
            return;
        }
        StringBuilder code = protoFile.getBody();
        code.append("enum ").append(cls.getSimpleName()).append(" {  \n");

        Field[] fields = cls.getFields();
        for (Field field : fields) {

            String name = field.getName();
            code.append(name).append("=");
            try {
                Enum value = Enum.valueOf(cls, name);
                if (value instanceof EnumReadable) {
                    code.append(((EnumReadable) value).value());
                } else {
                    code.append(value.ordinal());
                }
                code.append(";\n");
            } catch (Exception e) {
                continue;
            }
        }

        code.append("}\n ");
    }

    /**
     */
    private static String getFieldRequired(boolean required) {
        return "";
    }
}
